-module(active).

-export([behaviour_info/1]).
-export([start/3, start_link/3, view/1, create/3, call/3, cast/3, foldl/3, delete/2]).

-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).

-export([init_it/6]).

-record(db, {
	module,
	options,
	storage_mod,
	storage_data,
	active
}).

behaviour_info(callbacks) ->
	[{init,2}, {handle_call,3}, {handle_cast,2}, {handle_info,2}, {code_change,3}];
behaviour_info(_) ->
	undefined. 

%% api
start(Name, Module, Options) ->
	gen_server:start(Name, ?MODULE, {Module, Options}, []).
start_link(Name, Module, Options) ->
	gen_server:start_link(Name, ?MODULE, {Module, Options}, []).

view(Name) ->
	gen_server:cast(Name, '$view_db').
create(Name, Key, Args) ->
	gen_server:call(Name, {'$create_db', Key, Args}).
call(Name, Key, Request) ->
	gen_server:call(Name, {'$call_db', Key, Request}).
cast(Name, Key, Request) ->
	gen_server:call(Name, {'$cast_db', Key, Request}).
foldl(Name, Fun, Acc) ->
	gen_server:call(Name, {'$foldl_db', Fun, Acc}).
delete(Name, Key) ->
	gen_server:call(Name, {'$delete_db', Key}).

%% gen_server
init({Module, Options}) ->
    process_flag(trap_exit, true),
	StorageMod = proplists:get_value(storage, Options, active_dets_storage),
	{ok, #db{
		module = Module,
		options = Options,
		storage_mod = StorageMod,
		storage_data = StorageMod:open(Module, Options),
		active = gb_trees:empty()
	}}.

%% from API
handle_call({'$create_db', Key, Args}, _From, DB) ->
	case (DB#db.module):init(Key, Args) of
		{ok, Data} ->
			case activate(DB#db.module, Key, Data) of
				{ok, Pid, Timer} ->
					{reply, ok, ram_enter(DB, Key, {Pid, Timer})};
				Other ->
					{reply, Other, DB}
			end;
		Other ->
			{reply, Other, DB}
	end;
handle_call({'$call_db', Key, Request}, _From, DB) ->
	case frakenstein(DB, Key) of % been playing too much Castlvania: SOTN
		{ok, NDB, {Pid, _}} ->
			Reply = case catch gen:call(Pid, '$call_active', Request) of
				{ok, R} ->
					R;
				{'EXIT', Reason} ->
					exit({Reason, [?MODULE, call, Key, Request]})
			end,
			{reply, Reply, NDB};
		Other ->
			{reply, Other, DB}
	end;
handle_call({'$cast_db', Key, Request}, _From, DB) ->
	case frakenstein(DB, Key) of
		{ok, NDB, {Pid, _}} ->
			Msg = {'$cast_active', Request},
			case catch erlang:send(Pid, Msg, [noconnect]) of
				noconnect -> erlang:spawn(erlang, send, [Pid, Msg]);
				_ -> ok
			end,
			{reply, ok, NDB};
		Other ->
			{reply, Other, DB}
	end;
handle_call(_Event, _From, _DB) -> exit(bad_call).

handle_cast('$view_db', DB) ->
	lists:map(fun({K, {Pid, _} = V}) ->
		case gen:call(Pid, '$state_active', []) of
			{ok, {K, Data}} ->
				V;
			_ ->
				V
		end
	end, gb_trees:to_list(DB#db.active)),
	{noreply, DB};
handle_cast({'$delete_db', Key}, DB) ->
	case ram_lookup(DB, Key) of
		{value, {Pid, Timer} = V} ->
			timer:cancel(Timer),
			gen:call(Pid, '$terminate_active', []);
		_ -> ok
	end,
	{noreply, DB#db{
		storage_data = (DB#db.storage_mod):delete(DB#db.storage_data, Key)
	}};

%% from active row
handle_cast({'$store', Key, Data}, DB) ->
	{noreply, DB#db{
		storage_data = (DB#db.storage_mod):enter(DB#db.storage_data, Key, Data)
	}};
handle_cast(_Event, _DB) -> exit(bad_cast).

handle_info({'EXIT', Pid, {'$inactive', Key, _Reason}}, DB) ->
	DB0 = case ram_lookup(DB, Key) of
		{value, {Pid, Timer}} ->
			timer:cancel(Timer),
			ram_delete(DB, Key);
		_ ->
			DB
	end,
	{noreply, DB0};
handle_info(Event, DB) -> io:format("bad info ~p, state ~p~n", [Event, DB]), {noreply, DB}.

code_change(_, DB, _) -> {ok, DB}.
terminate(_Reason, DB) ->
	Storage = lists:foldl(fun(Pid, StorageData) ->
		case gen:call(Pid, '$terminate_active', []) of
			{ok, {Key, Data}} ->
				(DB#db.storage_mod):enter(StorageData, Key, Data);
			_ ->
				StorageData
		end
	end, DB#db.storage_data, gb_trees:values(DB#db.active)),
	(DB#db.storage_mod):close(Storage),
	ok.

%% internal
frakenstein(DB, Key) ->
	case ram_lookup(DB, Key) of
		{value, PidTimer} -> {ok, DB, PidTimer};
		none -> case (DB#db.storage_mod):lookup(DB#db.storage_data, Key) of
			{StorageData, {value, Data}} ->
				case activate(DB#db.module, Key, Data) of
					{ok, Pid, Timer} ->
						{ok, ram_enter(DB#db{storage_data = StorageData}, Key, {Pid, Timer}), {Pid, Timer}};
					Other ->
						Other
				end;
			{_StorageData, none} ->
				{error, {not_created, Key}}
		end
	end.

ram_enter(DB, Key, Pid) ->
	DB#db{
		active = gb_trees:enter(Key, Pid, DB#db.active)
	}.

ram_lookup(DB, Key) ->
	gb_trees:lookup(Key, DB#db.active).

ram_delete(DB, Key) ->
	DB#db{
		active = gb_trees:delete_any(Key, DB#db.active)
	}.

activate(Module, Key, Data) ->
	case gen:start(?MODULE, link, Module, {Key, Data}, []) of
		{ok, Pid} ->
			{ok, Timer} = timer:send_interval(timer:hms(0, 10, 0), Pid, '$store_active'),
			{ok, Pid, Timer};
		Error -> Error
	end.

%% active loop
init_it(Starter, Parent, Self, Module, {Key, State}, _) ->
	proc_lib:init_ack(Starter, {ok, Self}),
	loop(Parent, Module, 120000, Key, State).

loop(Parent, Module, Timeout, Key, State) ->
	receive Msg -> handle_msg(Msg, Parent, Module, Timeout, Key, State)
	after Timeout ->
		gen_server:cast(Parent, {'$store', Key, State}),
		exit({'$inactive', Key, timeout})
	end.

dispatch({'$cast_active', Msg}, Module, State) ->
	Module:handle_cast(Msg, State);
dispatch(Msg, Module,  State) ->
	Module:handle_info(Msg, State).

handle_msg({'$state_active', From, _Request}, Parent, Module, Timeout, Key, State) ->
	gen:reply(From, {Key, State}),
	loop(Parent, Module, Timeout, Key, State);
handle_msg({'$terminate_active', From, _Request}, _Parent, _Module, _Timeout, Key, State) ->
	gen:reply(From, {Key, State}),
	exit({'$inactive', Key, terminaated});
handle_msg({'$call_active', From, Request} = Msg, Parent, Module, Timeout, Key, State) ->
	case catch Module:handle_call(Request, From, State) of
		{reply, Reply, NState} ->
			gen:reply(From, Reply),
			loop(Parent, Module, Timeout, Key, NState);
		{reply, Reply, NState, NTimeout} ->
			gen:reply(From, Reply),
			loop(Parent, Module, NTimeout, Key, NState);
		Other ->
			handle_common_reply(Other, Msg, Parent, Module, Timeout, Key, State)
	end;
handle_msg('$store_active', Parent, Module, Timeout, Key, State) ->
	gen_server:cast(Parent, {'$store', Key, State}),
	loop(Parent, Module, Timeout, Key, State);
handle_msg(Msg, Parent, Module, Timeout, Key, State) ->
	Reply = (catch dispatch(Msg, Module, State)),
	handle_common_reply(Reply, Msg, Parent, Module, Timeout, Key, State).

handle_common_reply(Reply, _Msg, Parent, Module, Timeout, Key, _State) ->
	case Reply of
		{noreply, NState} ->
			loop(Parent, Module, Timeout, Key, NState);
		{noreply, NState, NTimeout} ->
			loop(Parent, Module, NTimeout, Key, NState);
		{'EXIT', What} ->
			exit({'$inactive', Key, What});
		_ ->
			exit({'$inactive', Key, {bad_return_value, Reply}})
	end.

